跳转至

你知道的.exe 文本编码架构分析与中文化方案(权威版 v4)

本文经 法国女人 + DebugView 运行时实测 双重验证,修正并取代 v3。 v3 的“编码模式恒为 1(SJIS)、靠切模式复用 UTF-8 解码器”的核心前提被运行时日志推翻,见 §0.1。 所有结论来自实际反汇编与实机日志,非推测。


0. TL;DR

中文化分两条独立路径,各有正确做法:

文本类型 渲染入口 正确中文化手段 原因
窄字符串(原 SJIS 硬编码日文) setTextByString @ 0x7C5E70 DLL 运行时 hook,按内容查表替换 UTF-8 运行时编码模式恒为 0(UTF-8),日文早已是合法 UTF-8,直接改传中文 UTF-8 即可
宽字符串(UTF-16LE 硬编码) setTextByWideString @ 0x7C5FD0 patch_utf16.py 就地二进制覆盖 EXE 字形来源是上游预构造的迭代器ws 指针仅供 wcslen;DLL 内 hook 换 ws 无效
  • 窄路径 DLL:exeTrans/build_dll/chusan_utf8.c(v4,内容替换),译表 chusan_trans.txt(UTF-8, 日文|中文)。
  • 宽路径工具:exeTrans/build_dll/patch_utf16.py + 词表 exeTrans/chutranslation/utf16_cn.txt
  • 未打补丁的原始 EXE;不再需要 EXEPatch.py 指针重定向,也不需要切编码模式。

0.1 v3 为什么错(运行时实测,2026-07-04)

DebugView 抓 hook 诊断日志证明: - setTextByString 运行时 textcodec_get_encoding_mode 返回值恒为 0(UTF-8),不是 v3 假设的 1(SJIS)。 - 游戏在更上游已把 SJIS 字面量转成 UTF-8;到达 setTextByString 时是合法 UTF-8 日文 (实测 “ロード中” = E3 83 AD E3 83 BC E3 83 89 E4 B8 AD),所以日文本就正常显示。 - v3 的 EXEPatch 把译文以 UTF-8 写入并重定向指针,再靠 hook 切模式——但模式本就是 0,译文 UTF-8 反被 当 SJIS 又解一遍 → 乱码(“加载中”显示成“蜉霓荳”)。且重定向的指针根本不在传给 setTextByStrings 里(字符串在更上游被拷贝/转码过)。 - 教训:本项目多次证明静态推断需运行时复核。窄/宽两条路径的最终定论都由实机日志裁定。


1. 分析方法论

  1. 打开 IDB(server_health 确认 hexrays_ready,imagebase 0x400000,module 你知道的.exe, IDB 路径 ---)。
  2. 用函数内自带日志字符串确证函数身份 ("[teaFontRenderer] setTextByString() : string 's' is too long." / "... setTextByWideString() : string 'ws' is too long.")。
  3. 反编译两条路径全链,逐个 thunk 跟到实体函数。
  4. 窄路径:DLL 加诊断日志(打印到达的字节、编码模式、指针范围),DebugView 实机抓取 → 定论 mode=0。
  5. 宽路径:反编译 setTextByWideStringsetTextDispatch→分支,追迭代器与 sub_44CEFB 的 布局状态初始化,判定字形来源为外部迭代器 a3ws 仅计长。
  6. 在 IDB 中重命名、加中文注释、修正签名并 idb_save

2. 窄字符串路径 teaFontRenderer_setTextByString @ 0x7C5E70

int __thiscall teaFontRenderer_setTextByString(void *this, const char *s, int arg)
{
  wide_buf = this[42];                 // 输出宽字符缓冲(this+168)
  if (!s) return 0;
  mode = textcodec_get_encoding_mode(g_textEncodingConfig);  // ★运行时实测恒为 0
  switch (mode) {
    case 0: // UTF-8  ← 现行运行时走这里
      len = strlen(s);
      if (len < this[3]) r = textcodec_utf8_to_utf16(wide_buf, s, len+1, 2*this[3]);
      else goto too_long;
      break;
    case 1: // Shift-JIS(现行版运行时不触发)
      len = strlen(s);
      if (len < this[3]) r = textcodec_sjis_to_wide(wide_buf, 2*this[3], s, len);
      else goto too_long;
      break;
    case 2: // EUC-JP
      ... textcodec_eucjp_to_sjis(...) 预处理后再 textcodec_sjis_to_wide(...);
      break;
    default: return 0;
  }
  if (r <= 0) return r;
  return (*(this->vtable[6]))(this, wide_buf, arg);   // vtable[6] = setTextDispatch
too_long:
  sub_40C11C("[teaFontRenderer] setTextByString() : string 's' is too long.");
  return 0;
}

关键s 到达时已是 UTF-8。窄路径中文化 = 无条件 hook 本函数,拿到达的 UTF-8 日文原文查表, 命中就把 s 换成中文 UTF-8 再调原函数。不切模式、不重定向指针、不用 EXEPatch。

三个解码器实体(判定证据见附录 §A): textcodec_utf8_to_utf16 @ 0xF20670textcodec_sjis_to_wide @ 0xF203A0textcodec_eucjp_to_sjis @ 0xF200A0;模式 getter textcodec_get_encoding_mode @ 0xF00260; 全局配置 g_textEncodingConfig @ 0x1C83DA40x7C5E8Bmov ecx,[0x1C83DA4] 证实)。


3. 宽字符串路径与“为什么 DLL hook 换 ws 无效”

3.1 vtable 布局(已在 0x18acec8 实测)

teaFontRenderer 虚表基址 0x18acec8(前含 RTTI 指针 0x019d2440):

index offset 槽地址 目标
5 +0x14 0x18acedc setTextByString(j_ 0x45a0bf
6 +0x18 0x18acee0 setTextDispatch(j_ 0x418c32

即 §2 里 vtable[6] = setTextDispatch

3.2 分发器 teaFontRenderer_setTextDispatch @ 0x7C5FA0

int __thiscall setTextDispatch(this, a2, a3) {
  if (*((BYTE*)this + 152))          return sub_419F1A(a2, a3);   // thunk→sub_7C7450
  if ((this[368] & 0xA0) == 0xA0)    return sub_7C6A60(a2, a3);   // 变体
  return setTextByWideString(this, a2, a3);
}

3.3 teaFontRenderer_setTextByWideString @ 0x7C5FD0(实测行为)

int __thiscall setTextByWideString(void *this, const wchar_t *ws, void *iter)
{
  if (!ws) return 0;
  len = wcslen(ws);                       // ★ws 唯一用途:计长,存入 v61[1]
  v61 = { ws, len, this[2], 颜色/坐标... };
  sub_44CEFB(v61);                        // = teaFontRenderer_layout_state_init: state+4=ws, state+8=len
  ...
  it = iter;                              // ★字形真正来源
  next = (*it)->vtable[1];                // iter->next
  while (next(it, &tok)) {                // 逐 token 取字符码 tok
    switch (tok.type) {                   // 0=普通字符 1=外字 2=颜色 3=换行 4=内嵌精灵
      ...  tok 里的字符码查字形 GetGlyphByCode, 写入字形数组 this[43] ...
    }
  }
}
  • ws 只喂 wcslensub_44CEFB(=teaFontRenderer_layout_state_init @ 0xF09C20) 虽把 ws 拷进布局状态对象 [state+4],但那只是布局度量,逐字符字形码来自外部迭代器 iter(a3)
  • iter上游调用点预先构造好的带虚表迭代器,通过 setTextByStringsetTextDispatch 的第 3 参 (arg)一路透传下来;它不在 setTextByWideString 内从 ws 派生。

3.4 结论:宽路径不能靠 DLL 换 ws 中文化

若在 DLL 里 hook setTextByWideString 并替换 ws 指针: - 只会改变 wcslen 测得的长度,不改变实际渲染的字形(字形来自 iter); - 结果是长度与内容错位 → 截断/错乱,等同 v3 曾犯的“假设错误的入口”。

宽字符字面量的迭代器在众多上游调用点各自构造(例:その他 作为全局 wchar_t*sub_431E30 注册进字符串表 dword_1C9ADC0,再于各处包装成迭代器渲染),没有单一窄口可像 setTextByString 那样统一拦截替换。

因此宽字符串中文化用 EXEpatch.py 精简版(见 §4),而非扩展 DLL 加 chusan_trans_w.txt。UTF-16LE 原生支持中文码点。


4. 落地方案

4.1 窄字符串(DLL 内容替换,v4)

  • 源码 exeTrans/build_dll/chusan_utf8.c:init 时按 EXE 目录加载 chusan_trans.txt(UTF-8,日文|中文), 在 setTextByString 入口精确匹配到达的 UTF-8 日文,命中则改传中文 UTF-8。
  • Hook 点 0x7C5E70,前 5 字节 55 56 8B F1 57push ebp;push esi;mov esi,ecx;push edi),JMP 边界安全。
  • 调用约定用 fastcall + 哑元 EDX 模拟 thiscall(callee 清栈 ret 8)。
  • 编译 buildv3.bat(MSVC x86;本机 MinGW-w64 仅 64 位,链不了 32 位)。
  • 注入:inject_x86 -d -k chusanhook_x86.dll -k chusan_utf8.dll 你知道的.exe

4.2 宽字符串(就地二进制覆盖)

  • 工具 exeTrans/build_dll/patch_utf16.py,词表 exeTrans/chutranslation/utf16_cn.txt(UTF-8,日文|中文)。
  • 约束:中文 UTF-16 码元数 ≤ 日文,才能原地覆盖(末尾补 0x0000 截断);更长的串脚本会报 TOOLONG 跳过。
  • 用法:python patch_utf16.py <exe> utf16_cn.txt(干跑报告)/ 加 --apply 实改(先备份 .utf16bak)。
  • 就地覆盖不新增 section、不移动代码,0x7C5E70 等 RVA 不变,窄路径 hook 不受影响。

4.3 字符串提取(辅助)

exeTrans/build_dll/strings_dump.py:扫 .rdata/.data,按编码(ASCII/SJIS/UTF-16LE/UTF-8)分文件导出, 要求 null 结尾 + 文本白名单去噪。用于枚举待翻译串来源、区分窄/宽路径归属。


5. 关键地址表

符号 VA RVA(基址0x400000) 说明
teaFontRenderer_setTextByString 0x7C5E70 0x3C5E70 窄入口;DLL hook 点
teaFontRenderer_setTextByWideString 0x7C5FD0 0x3C5FD0 宽入口;ws 仅计长,字形来自迭代器
teaFontRenderer_setTextDispatch 0x7C5FA0 0x3C5FA0 宽路径分发(= 窄路径 vtable[6])
teaFontRenderer_layout_state_init 0xF09C20 0xB09C20 从 {ws,len,...} 初始化布局状态
textcodec_utf8_to_utf16 0xF20670 0xB20670 UTF-8→UTF-16
textcodec_sjis_to_wide 0xF203A0 0xB203A0 SJIS→wide
textcodec_eucjp_to_sjis 0xF200A0 0xB200A0 EUC-JP→SJIS
textcodec_get_encoding_mode 0xF00260 0xB00260 读模式(运行时=0)
g_textEncodingConfig 0x1C83DA4 0x1883DA4 全局配置对象指针
teaFontRenderer vtable 基址 0x18ACEC8 前含 RTTI 0x019D2440

6. 本次在 IDB 中所做的标注

  • 重命名:sub_F09C20teaFontRenderer_layout_state_init
  • 0x7C5FD0/0x7C6035/0xF09C20 加中文注释,记录“ws 仅计长、字形来自外部迭代器、宽路径应用 patch_utf16.py 就地覆盖”的结论。
  • idb_save

附录 A. 三个解码器判定证据

  • textcodec_utf8_to_utf16 @ 0xF20670byte_1924638[]=UTF-8 尾随字节数表; (*p & 0xC0)!=0x80 判 continuation byte;>0x10FFFF 越界;>0x10000 生成代理对 → 标准 ConvertUTF UTF-8→UTF-16。
  • textcodec_sjis_to_wide @ 0xF203A0:首字节 0x81–0x9F/0xE0–0xFC = SJIS 双字节首字节区间。
  • textcodec_eucjp_to_sjis @ 0xF200A00x8F=SS3、0x8E=SS2;((b&0x7F)<<8)+(b2&0x7F) 取 JIS 码位。

评论